<?php
/**
 * Copyright 2011 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 *
 * Class to decrypt and verify credit card number and CVC information used
 * in the Google Payment Express API.
 *
 * @author Madhusudhan J V <jampala@google.com>
 * @author Shawn Willden <swillden@google.com>
 * @author Nadim Ratani <nratani@google.com>
 * @version 1.0
 * @package payment-express
 */

/**
 * The Decrypter class performs both decryption and signature verification.
 * @package google
 * @subpackage paymentexpress
 */
class Decrypter {

  var $sessionData;
  var $fieldData;

  var $merchantPrivateRSAKey;
  var $merchantPrivateRSAKeyPassphrase;

  var $googlePublicDSAKey;

  /**
   * Decrypts the given JSON encoded string.
   * @param string $encryptedData Encrypted data.
   * @return string Plaintext after decrypting.
   */
  function decrypt($encryptedData) {
    $jsonData = json_decode($encryptedData);

    // Decode and decrypt the session data
    $this->sessionData = $this->getSessionData($jsonData->session_data);

    // Decode and decrypt the field data
    $this->fieldData = $this->getfieldData($jsonData->field_data);

    // return the plaintext
    return $this->getPlainText();
  }

  /**
   * Decodes and decrypts the session data.
   * @param string $sessionData Session data from the message received.
   * @return Object Object having key value pairs of session data.
   */
  function getSessionData($sessionData) {
    // Decode the session data using base64 web safe method
    $data = $this->base64DecodeWS($sessionData);

    // Extract the session data
    $pKey = openssl_pkey_get_private($this->merchantPrivateRSAKey, $this->merchantPrivateRSAKeyPassphrase);
    $bool = openssl_private_decrypt(substr($data, 5), $jsonData, $pKey,
      OPENSSL_PKCS1_OAEP_PADDING);

    while ($msg = openssl_error_string()) {
      $this->raiseException($msg);
    }

    $jsonData = json_decode($jsonData);
    $jsonData->version = substr($data, 0, 1);
    $jsonData->sha1 = substr($data, 1, 4);
    return $jsonData;
  }

  /**
   * Decodes and saves the field data as an object.
   * @param string $fieldData Field data from the message received.
   * @return Object Object having key value pairs of field data.
   */
  function getfieldData($fieldData) {
    // Decode the field data using base64 web safe method
    $decodedData = $this->base64DecodeWS($fieldData);
    // Get the data length value which is in Big endian format
    $dataLength = $this->getBigEndianData(substr($decodedData, 5, 4));

    return (object) array(
      'version' => substr($decodedData, 0, 1),
      'sha1' => substr($decodedData, 1, 4),
      'dataLength' => $dataLength,
      'signaturedData' => substr($decodedData, 9, $dataLength),
      'signature' => substr($decodedData, $dataLength + 9)
    );
  }

  /**
   * Decrypt the data and returns the plaintext.
   * @return string Plaintext.
   */
  function getPlainText() {
    // Get key details from session data
    $sessionData = $this->sessionData;
    $kSize = $sessionData->key->size;
    $aesKey = $sessionData->key->aesKeyString;
    $mode = $sessionData->key->mode;
    $b64AesKey = $this->base64DecodeWS(strtr($aesKey, '-_', '+/'));
    $offset = 5 + $kSize / 8;

    // Other values needed to extract plaintext
    $fieldData = $this->fieldData;
    $iv = substr($fieldData->signaturedData, 5, $kSize / 8);
    $encryptedData = substr($fieldData->signaturedData, $offset,
      $fieldData->dataLength - ($offset + 20));
    if ($this->validateSignatureData()) {
      return $this->aes128cbcDecrypt($b64AesKey, $encryptedData, $iv, $mode);
    } else {
      $this->raiseException('Signature is not valid.');
    }
  }

  /**
   * Validates the signature.
   * @return boolean Whether signature is valid or not.
   */
  function validateSignatureData() {
    $cData = $this->fieldData->signaturedData;
    $nonce = $this->base64DecodeWS($this->sessionData->nonce);
    $toVerify = $cData . pack('N', 16) . $nonce . pack('x');
    $pKey = openssl_pkey_get_public($this->googlePublicDSAKey);
    return openssl_verify($toVerify, $this->fieldData->signature, $pKey,
      OPENSSL_ALGO_DSS1);
  }

  /**
   * Decrypts the signed text using the aeskey from session data.
   * @param string $key AES key.
   * @param string $encryptedData Data to be decrypted.
   * @param string $iv Initialization vector.
   * @return string Plaintext.
   */
  function aes128cbcDecrypt($key, $encryptedData, $iv, $mode) {
    if (!function_exists('mcrypt_decrypt')) {
      $this->raiseException('This library requires the Mcrypt library.');
    }
    $decrypted = mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $key, $encryptedData,
      MCRYPT_MODE_CBC, $iv);

    // Remove extra data link escape characters
    return trim(trim($decrypted), "\x00..\x1F");
  }

  /**
   * Converts a big endian string to an integer.
   * @param string $bin Binary string representing the integer.
   * @return integer The integer.
   */
  function getBigEndianData($bin) {
    if (PHP_INT_SIZE <= 4) {
      list(, $h, $l) = unpack('n*', $bin);
      return ($l + ($h * 0x010000));
    } else {
      list(, $int) = unpack('N', $bin);
      return $int;
    }
  }

  /**
   * Gets the merchants private RSA key from the given location and stores
   * it in a global variable.
   * @param string $keyPath Path to the merchants private RSA key file.
   */
  function setMerchantPrivateKey($keyPath) {
    $this->merchantPrivateRSAKey = $this->getDataFromFile($keyPath);
  }

  /**
   * Sets a passphrase for the Merchants private key. This parameter will be
   * used when reading the private key.
   * @param string $passphrase The passphrase for the merchants private RSA
   * key file.
   */
  function setPrivateKeyPassphrase($passphrase) {
    $this->merchantPrivateRSAKeyPassphrase = $passphrase;
  }

  /**
   * Gets Google's public DSA key from given location and stores it in a 
   * global variable.
   * @param string $keyPath Path to Google's public DSA key file
   */
  function setGooglePublicKey($keyPath) {
    $this->googlePublicDSAKey = $this->getDataFromFile($keyPath);
  }

  /**
   * Reads the data from given file path.
   * @param string $keyPath Path of the file to read.
   * @return string Content read from the given path.
   */
  function getDataFromFile($path) {
    $dataHandle = fopen($path, 'r' );
    if (!$dataHandle) {
      $this->raiseException('Not able to read file: ' . $path);
    }
    return file_get_contents($path);
  }

  /**
   * Decode given data using base64 web safe method.
   * @param string $content Data to be decoded.
   * @return string Decoded data.
   */
  function base64DecodeWS($content) {
    return base64_decode(strtr($content, '-_', '+/'));
  }

  /**
   * Raise exception and logs the given message.
   * @param string $msg Message to be displayed.
   */
  function raiseException($msg) {
    die($msg);
  }
}
